1
|
|
|
import qs from 'qs'; |
2
|
|
|
import { isEmpty, isNil, reject } from 'ramda'; |
3
|
|
|
import { AxiosInstance, AxiosResponse, Method } from 'axios'; |
4
|
|
|
import { ShortUrlsListParams } from '../../short-urls/reducers/shortUrlsListParams'; |
5
|
|
|
import { ShortUrl, ShortUrlData } from '../../short-urls/data'; |
6
|
|
|
import { OptionalString } from '../utils'; |
7
|
|
|
import { |
8
|
|
|
ShlinkHealth, |
9
|
|
|
ShlinkMercureInfo, |
10
|
|
|
ShlinkShortUrlsResponse, |
11
|
|
|
ShlinkTags, |
12
|
|
|
ShlinkTagsResponse, |
13
|
|
|
ShlinkVisits, |
14
|
|
|
ShlinkVisitsParams, |
15
|
|
|
ShlinkShortUrlMeta, |
16
|
|
|
ShlinkDomain, |
17
|
|
|
ShlinkDomainsResponse, |
18
|
|
|
ShlinkVisitsOverview, |
19
|
|
|
} from './types'; |
20
|
|
|
|
21
|
24 |
|
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : ''; |
22
|
2 |
|
const rejectNilProps = reject(isNil); |
23
|
|
|
|
24
|
|
|
export default class ShlinkApiClient { |
25
|
|
|
private apiVersion: number; |
26
|
|
|
|
27
|
|
|
public constructor( |
28
|
|
|
private readonly axios: AxiosInstance, |
29
|
|
|
private readonly baseUrl: string, |
30
|
|
|
private readonly apiKey: string, |
31
|
|
|
) { |
32
|
28 |
|
this.apiVersion = 2; |
33
|
|
|
} |
34
|
|
|
|
35
|
28 |
|
public readonly listShortUrls = async (params: ShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> => |
36
|
1 |
|
this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', params) |
37
|
1 |
|
.then(({ data }) => data.shortUrls); |
38
|
|
|
|
39
|
28 |
|
public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => { |
40
|
4 |
|
const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options as any); |
41
|
|
|
|
42
|
2 |
|
return this.performRequest<ShortUrl>('/short-urls', 'POST', {}, filteredOptions) |
43
|
2 |
|
.then((resp) => resp.data); |
44
|
|
|
}; |
45
|
|
|
|
46
|
28 |
|
public readonly getShortUrlVisits = async (shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits> => |
47
|
1 |
|
this.performRequest<{ visits: ShlinkVisits }>(`/short-urls/${shortCode}/visits`, 'GET', query) |
48
|
1 |
|
.then(({ data }) => data.visits); |
49
|
|
|
|
50
|
28 |
|
public readonly getTagVisits = async (tag: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> => |
51
|
1 |
|
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query) |
52
|
1 |
|
.then(({ data }) => data.visits); |
53
|
|
|
|
54
|
28 |
|
public readonly getVisitsOverview = async (): Promise<ShlinkVisitsOverview> => |
55
|
1 |
|
this.performRequest<{ visits: ShlinkVisitsOverview }>('/visits', 'GET') |
56
|
1 |
|
.then(({ data }) => data.visits); |
57
|
|
|
|
58
|
28 |
|
public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> => |
59
|
3 |
|
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain }) |
60
|
3 |
|
.then(({ data }) => data); |
61
|
|
|
|
62
|
28 |
|
public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> => |
63
|
3 |
|
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain }) |
64
|
|
|
.then(() => {}); |
65
|
|
|
|
66
|
28 |
|
public readonly updateShortUrlTags = async ( |
67
|
|
|
shortCode: string, |
68
|
|
|
domain: OptionalString, |
69
|
|
|
tags: string[], |
70
|
|
|
): Promise<string[]> => |
71
|
3 |
|
this.performRequest<{ tags: string[] }>(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags }) |
72
|
3 |
|
.then(({ data }) => data.tags); |
73
|
|
|
|
74
|
28 |
|
public readonly updateShortUrlMeta = async ( |
75
|
|
|
shortCode: string, |
76
|
|
|
domain: OptionalString, |
77
|
|
|
meta: ShlinkShortUrlMeta, |
78
|
|
|
): Promise<ShlinkShortUrlMeta> => |
79
|
3 |
|
this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta) |
80
|
3 |
|
.then(() => meta); |
81
|
|
|
|
82
|
28 |
|
public readonly listTags = async (): Promise<ShlinkTags> => |
83
|
1 |
|
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' }) |
84
|
1 |
|
.then((resp) => resp.data.tags) |
85
|
1 |
|
.then(({ data, stats }) => ({ tags: data, stats })); |
86
|
|
|
|
87
|
28 |
|
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> => |
88
|
1 |
|
this.performRequest('/tags', 'DELETE', { tags }) |
89
|
1 |
|
.then(() => ({ tags })); |
90
|
|
|
|
91
|
28 |
|
public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> => |
92
|
1 |
|
this.performRequest('/tags', 'PUT', {}, { oldName, newName }) |
93
|
1 |
|
.then(() => ({ oldName, newName })); |
94
|
|
|
|
95
|
28 |
|
public readonly health = async (): Promise<ShlinkHealth> => |
96
|
1 |
|
this.performRequest<ShlinkHealth>('/health', 'GET') |
97
|
1 |
|
.then((resp) => resp.data); |
98
|
|
|
|
99
|
28 |
|
public readonly mercureInfo = async (): Promise<ShlinkMercureInfo> => |
100
|
1 |
|
this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET') |
101
|
1 |
|
.then((resp) => resp.data); |
102
|
|
|
|
103
|
28 |
|
public readonly listDomains = async (): Promise<ShlinkDomain[]> => |
104
|
1 |
|
this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data); |
105
|
|
|
|
106
|
28 |
|
private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> => { |
107
|
24 |
|
try { |
108
|
24 |
|
return await this.axios({ |
109
|
|
|
method, |
110
|
|
|
url: `${buildShlinkBaseUrl(this.baseUrl, this.apiVersion)}${url}`, |
111
|
|
|
headers: { 'X-Api-Key': this.apiKey }, |
112
|
|
|
params: rejectNilProps(query), |
113
|
|
|
data: body, |
114
|
|
|
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }), |
115
|
|
|
}); |
116
|
|
|
} catch (e) { |
117
|
|
|
const { response } = e; |
118
|
|
|
|
119
|
|
|
// Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error |
120
|
|
|
// when performed from the browser (due to the preflight request not returning a 2xx status. |
121
|
|
|
// See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here. |
122
|
|
|
// The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as |
123
|
|
|
// if a request has been performed to a not supported API version. |
124
|
|
|
const apiVersionIsNotSupported = !response; |
125
|
|
|
|
126
|
|
|
// When the request is not invalid or we have already tried both API versions, throw the error and let the |
127
|
|
|
// caller handle it |
128
|
4 |
|
if (!apiVersionIsNotSupported || this.apiVersion === 1) { |
129
|
|
|
throw e; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
this.apiVersion = this.apiVersion - 1; |
133
|
|
|
|
134
|
|
|
return await this.performRequest(url, method, query, body); |
135
|
|
|
} |
136
|
|
|
}; |
137
|
|
|
} |
138
|
|
|
|